<!--
    * @Author: 叶敏轩 mc20000406@163.com
    * @Date: 2024-01-23 12:01:30
 * @LastEditors: 叶敏轩 mc20000406@163.com
 * @LastEditTime: 2024-01-30 01:25:56
 * @FilePath: /waterfall/src/waterfall.vue
    * @Description: 
    -->
<template>
  <div
    class="waterfall waterfall-scroll-container"
    ref="waterfallContainer"
    @scroll.passive="handleScroll"
  >
    <div
      v-for="(item, index) in visibleItems"
      :key="index"
      class="waterfallItem waterfall-scroll-item"
      :data-index="item.dataIndex"
      :style="{
        transform: transformWaterfallItem(item.dataIndex),
        width: item.blockWidth + 'px',
      }"
    >
      <slot :item="item" name="item" ref="itemContent"> </slot>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed, watch } from "vue";
import type { Ref } from "vue";
interface Props {
  data: any; //数据
  waterfallIndex?: string; //瀑布流索引
  col?: number | null; //列数
  visibleItemCount: number; //显示数量
  gutterWidth: number; //间隔距离
  columnImgPadding: number; //图片内边距
  contentHeight: number; //文字内容高度
  calcItemHeight?: (item: any, itemWidth: number) => number;
}
interface Column {
  translateX: number;
}
interface WaterFallItem {
  dataIndex: number; //项目索引
  blockWidth: number; //项目宽度
  imgWidth: number; //图片宽度
  imgHeight: number; //图片高度
  translateY: number; //项目y轴位移
  translateX: number; //项目x轴位移
  src: string; //图片路径
  title?: string;
  value?: string;
  colmnIndex: number; //保存的列数索引
}
// @ts-ignore
const props = withDefaults(defineProps<Props>(), {
  data: () => [],
  waterfallIndex: "",
  col: null,
  gutterWidth: 0,
  visibleItemCount: 20,
  columnImgPadding: 0,
  calcItemHeight: (item: any, itemWidth: number) => 250,
});
const waterfallContainer = ref();
const trueCol = ref(0);
const heightArr = ref([]) as any;
const dataArr: Ref<WaterFallItem[]> = ref([]) as any;
const columns: Ref<Column[]> = ref([]);
const columnWidth: Ref<number | null> = ref(null);
//显示区间起始索引
const startRow = ref(0);
//显示区间结束索引
const endRow = ref(3);
//项目总数，用于区间计算
let total = ref(0);
//是否该区间项目已经全部加载完成
let loadComplete = ref(false);
//是否已经滚动到底部
const isToEnd = ref(false);
//显示项
const visibleItems = computed(() => {
  dataArr.value.forEach((item: any, index: number) => {
    item["dataIndex"] = index;
  });
  return dataArr.value.slice(
    startRow.value * props.visibleItemCount,
    endRow.value * props.visibleItemCount
  );
});
const init = async () => {
  return new Promise<void>((resolve: any) => {
    let col = 0;
    let divWidth = 0;
    let translateX = 0;
    if (props.col == null) {
      col = 5;
    } else {
      col = props.col;
    }
    trueCol.value = col;
    heightArr.value = new Array(col); // 创建数组用来缓存高度
    heightArr.value.fill(0);
    nextTick(() => {
      let newColumns: any = [];
      //计算每列的宽度和x轴位移距离
      for (var i = 0; i < col; i++) {
        divWidth =
          waterfallContainer.value.clientWidth / col -
          (props.gutterWidth * (col - 1)) / col;
        if (i != 0) {
          translateX = divWidth * i + props.gutterWidth * i;
        } else {
          translateX = 0;
        }
        newColumns.push({
          translateX: translateX,
        });
      }
      columns.value = newColumns;
      columnWidth.value = divWidth;
      resolve();
    });
  });
};

//添加项
const addItem = async () => {
  loadComplete.value = false;
  let promiseArr = [];
  let time = 0;
  const itemsToLoad = Math.min(20, props.data.length - total.value);
  for (let i = total.value; i < total.value + itemsToLoad; i++) {
    promiseArr.push(
      new Promise<void>(async (resolve) => {
        setTimeout(() => {
          addVisibleItem(props.data[i], i);
          resolve();
        }, 100 * time);
        time += 1;
      })
    );
  }
  total.value += itemsToLoad;
  //等待当前区间的项全部加载完成
  await Promise.all(promiseArr);
  loadComplete.value = true;
};
const transformWaterfallItem = computed(() => {
  return (index: number) => {
    return (
      "translateX(" +
      dataArr.value[index].translateX +
      "px)" +
      "translateY(" +
      dataArr.value[index].translateY +
      "px)"
    );
  };
});
const addVisibleItem = async (item: any, index: number) => {
  let newImg = new Image();
  newImg.src = item.imgUrl;
  if (newImg.complete) {
    let trueWidth = columnWidth.value;
    if (trueWidth) {
      let imgColumnHeight = (newImg.height * trueWidth) / newImg.width;
      let minHeight = Math.min(...heightArr.value);
      let minHeightIndex = heightArr.value.indexOf(minHeight);
      const textContentHeight = props.calcItemHeight(
        {
          dataIndex: index,
          blockWidth: trueWidth,
          src: newImg.src,
          imgHeight: imgColumnHeight,
          title: item.title,
          value: item.value,
        },
        trueWidth
      );
      heightArr.value[minHeightIndex] += textContentHeight + props.gutterWidth;
      dataArr.value.push({
        dataIndex: index,
        colmnIndex: minHeightIndex,
        blockWidth: trueWidth,
        imgWidth: trueWidth - props.columnImgPadding * 2,
        imgHeight: imgColumnHeight,
        translateY: minHeight,
        translateX: columns.value[minHeightIndex].translateX,
        src: newImg.src,
        title: item.title,
        value: item.value,
      });
    }
  } else {
    await new Promise<any>((resolve: any) => {
      //等待图片加载
      newImg.onload = function () {
        let trueWidth = columnWidth.value;
        if (trueWidth) {
          let imgColumnHeight = (newImg.height * trueWidth) / newImg.width;
          let minHeight = Math.min(...heightArr.value);
          let minHeightIndex = heightArr.value.indexOf(minHeight);
          const textContentHeight = props.calcItemHeight(
            {
              dataIndex: index,
              blockWidth: trueWidth,
              src: newImg.src,
              imgHeight: imgColumnHeight,
              title: item.title,
              value: item.value,
            },
            trueWidth
          );
          console.log(textContentHeight, imgColumnHeight, props.gutterWidth);

          heightArr.value[minHeightIndex] +=
            props.gutterWidth + textContentHeight;
          dataArr.value.push({
            dataIndex: index,
            colmnIndex: minHeightIndex,
            blockWidth: trueWidth,
            imgWidth: trueWidth - props.columnImgPadding * 2,
            imgHeight: imgColumnHeight,
            translateY: minHeight,
            translateX: columns.value[minHeightIndex].translateX,
            src: newImg.src,
            title: item.title,
            value: item.value,
          });
        }
        resolve();
      };
      newImg.onerror = function () {
        resolve();
      };
    });
  }
};
const handleScroll = (e: Event) => {
  // 获取滚动元素的滚动高度、可视高度和已滚动距离
  const scrollHeight = (e.target as HTMLElement).scrollHeight;
  const clientHeight = (e.target as HTMLElement).clientHeight;
  const scrollTop = (e.target as HTMLElement).scrollTop;
  //判断向上滚动是否到达当前区间的最小高度，如果到达了就将显示区间的索引减少
  const sectionDataArr = dataArr.value.slice(
    startRow.value * props.visibleItemCount,
    endRow.value * props.visibleItemCount
  );
  const minY = sectionDataArr.reduce(
    (min, item) => Math.min(min, item.translateY),
    Infinity
  );
  if (scrollTop <= minY && scrollTop != 0) {
    startRow.value--;
    endRow.value--;
  }
  // 判断是否滚动到底部，如果到达了就将显示区间的索引增加
  console.log(
    scrollTop + clientHeight,
    scrollHeight,
    Math.max(...heightArr.value)
  );

  if (
    scrollTop + clientHeight >= scrollHeight &&
    scrollHeight < Math.max(...heightArr.value) + 20
  ) {
    //如果当前区间的项全部加载完成，才可以触发滚动加载
    if (loadComplete.value) {
      isToEnd.value = true;
      updateVisibleItem();
    }
  }
};
const updateVisibleItem = () => {
  isToEnd.value = false;
  addItem();
  if (endRow.value <= Math.ceil(props.data.length / props.visibleItemCount)) {
    endRow.value++;
    if (endRow.value - startRow.value > 4) {
      startRow.value++;
    }
  }
};
//自适应方法
const resize = async () => {
  let col = 0;
  if (props.col == null) {
    col = 5;
  } else {
    col = props.col;
  }
  let arr: any = [];
  const stashArr = new Array(col);
  stashArr.fill(0);
  for (let i = 0; i < dataArr.value.length; i++) {
    let newImg = new Image();
    newImg.src = dataArr.value[i].src;
    await new Promise<any>((resolve: any) => {
      //等待图片加载
      newImg.onload = function () {
        let trueWidth = columnWidth.value;
        if (trueWidth) {
          const textContentHeight = props.calcItemHeight(
            dataArr.value[i],
            trueWidth
          );
          let imgColumnHeight = (newImg.height * trueWidth) / newImg.width;
          let minHeight = Math.min(...stashArr);
          let minHeightIndex = stashArr.indexOf(minHeight);
          stashArr[minHeightIndex] += props.gutterWidth + textContentHeight;
          arr.push({
            dataIndex: i,
            colmnIndex: minHeightIndex,
            blockWidth: trueWidth,
            imgWidth: trueWidth - props.columnImgPadding * 2,
            imgHeight: imgColumnHeight,
            translateY: minHeight,
            translateX: columns.value[minHeightIndex].translateX,
            src: newImg.src,
            title: dataArr.value[i].title,
            value: dataArr.value[i].value,
          });
        }
        resolve();
      };
      newImg.onerror = function () {
        resolve();
      };
    });
  }
  dataArr.value = arr;
  heightArr.value = stashArr;
};
onMounted(async () => {
  await init();
  await addItem();
  window.addEventListener("resize", async () => {
    await init();
    resize();
  });
});
watch(
  () => loadComplete.value,
  (newVal) => {
    if (newVal == true && isToEnd.value == true) {
      updateVisibleItem();
    }
  }
);
</script>
<style scoped lang="less">
.waterfall-scroll-container {
  overflow-y: scroll;
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-wrap: wrap;
  .waterfall-scroll-item {
    position: absolute;
    display: flex;
    align-items: center;
    justify-content: center;
  }
}
</style>
